/*                                      _
 *     ___ _ __ ___ _ __ ___   ___   __| |
 *    / __| '__/ __| '_ ` _ \ / _ \ / _` |
 *    \__ \ |  \__ \ | | | | | (_) | (_| |
 *    |___/_|  |___/_| |_| |_|\___/ \__,_|                                  
 *
 *	   Infected Despawner Extraordinaire
 */

#define MODULE_NAME "DespawnInfected"
#define GAMECONFIG_FILE "srsmod"

#define TRACE_TOLERANCE 75.0
#define ZOMBIE_CHECK_INTERVAL 3.0

new Handle:fGetInfFlowDist = INVALID_HANDLE;
new Handle:fPlayerGetFlowDistance = INVALID_HANDLE;

new Handle:cvarDespawnInfected = INVALID_HANDLE;
new Handle:cvarDespawnDistance = INVALID_HANDLE;
new Handle:cvarRespawnRemovedCI = INVALID_HANDLE;

DespawnInfected_OnPluginStart()
{
	_DI_PrepareSDKCalls();

	CreateTimer(ZOMBIE_CHECK_INTERVAL, DespawnInfected_Check_Timer, 0, TIMER_REPEAT); // this is our Zombie Checking Call
	
	cvarDespawnInfected = CreateConVar("l4d2_srs_ci_despawn", "1", " Enable or Disable the leftbehind CI de-spawner functionality ", SRS_CVAR_DEFAULT_FLAGS);
	cvarDespawnDistance = CreateConVar("l4d2_srs_ci_despawn_distance", "2500.0", " Distance Setting for CI de-spawner functionality in game float units ", SRS_CVAR_DEFAULT_FLAGS);
	cvarRespawnRemovedCI = CreateConVar("l4d2_srs_ci_respawn", "0", " Enable or Disable respawning of de-spawned Common Infected ", SRS_CVAR_DEFAULT_FLAGS);
}

_DI_PrepareSDKCalls()
{
	new Handle:conf = LoadGameConfigFile(GAMECONFIG_FILE);
	
	if(conf != INVALID_HANDLE) Debug_Print("[SRSMOD] %s.txt Gamedata file loaded", GAMECONFIG_FILE);
	else ThrowError("[SRSMOD] Failed to load %s.txt in gamedata folder", GAMECONFIG_FILE);
	
	StartPrepSDKCall(SDKCall_Entity);
	Debug_Print("[SRSMOD] GetInfectedFlowDistance Call prepped");
	
	new bool:bGetInfFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, "GetInfectedFlowDistance");
	if(!bGetInfFlowDistFuncLoaded) ThrowError("[SRSMOD] Could not load the GetInfectedFlowDistance signature");
	
	PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain);
	Debug_Print("GetInfectedFlowDistance Signature prepped");
	fGetInfFlowDist = EndPrepSDKCall();
	
	if(fGetInfFlowDist == INVALID_HANDLE) ThrowError("[SRSMOD] Could not prep the GetInfectedFlowDistance function");	
	
	StartPrepSDKCall(SDKCall_Player);
	Debug_Print("[SRSMOD] PlayerGetFlowDistance Call prepped");
	
	new bool:bPGetFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, "PlayerGetFlowDistance");
	if(!bPGetFlowDistFuncLoaded) ThrowError("[SRSMOD] Could not load the PlayerGetFlowDistance signature");
	
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain);
	Debug_Print("[SRSMOD] PlayerGetFlowDistance Signature prepped");
	fPlayerGetFlowDistance = EndPrepSDKCall();
	
	if(fPlayerGetFlowDistance == INVALID_HANDLE) ThrowError("[SRSMOD] Could not prep the PlayerGetFlowDistance function");
}

// Infected::GetFlowDistance(void)const
Float:L4D2_GetInfectedFlowDistance(entity)
{
	return SDKCall(fGetInfFlowDist, entity);
}

// CTerrorPlayer::GetFlowDistance(TerrorNavArea::FlowType)const
Float:L4D2_GetPlayerFlowDist(client) // integer flowtype does not actually have an effect, 0 or 1 or anything work fine
{
	return SDKCall(fPlayerGetFlowDistance, client, 0);
}

public Action:DespawnInfected_Check_Timer(Handle:timer)
{
	if (!GetConVarBool(cvarDespawnInfected)) return;
	if (!IsVersus()) return;
	if (!survivorCount) return; // zero Survivor case

	new Float:lastsurvivorflow = 0.0;
	new Float:checkagainst = 0.0;
	new bool:foundone = false;
	
	for (new i = 1; i <= survivorCount; i++) // this loop finds us the lowest Survivor Flow
	{
		if (!IsValidEntity(survivorIndex[i]))
		{
			StateHelpers_RebuildSurvIndex();
			return;
		}

		checkagainst = L4D2_GetPlayerFlowDist(survivorIndex[i]);
		
		if (checkagainst < lastsurvivorflow || lastsurvivorflow == 0.0)
		{
			lastsurvivorflow = checkagainst;
			foundone = true;
		}
	}
	
	if (!foundone) return; // no valid Survivor Flow found? abort

	if (lastsurvivorflow < GetConVarFloat(cvarDespawnDistance))
	{
		Debug_Print("Lowest Flow Survivor (%f) found in Map Start Area, aborting", lastsurvivorflow);
		return;
	}

	new MaxEnts = GetMaxEntities();
	decl String:entclass[128];
	decl Float:zombieflow;
	
	for (new i = 32; i <= MaxEnts; i++)
	{
		if (!IsValidEntity(i)) continue; // ent validity checkagainst
		
		GetEdictClassname(i, entclass, sizeof(entclass));
		if (!StrEqual(entclass, "infected", false)) continue; // and BAM its a zombie
		
		zombieflow = L4D2_GetInfectedFlowDistance(i);
		
		if (lastsurvivorflow - GetConVarFloat(cvarDespawnDistance) > zombieflow) // if the Zombie is left far behind
		{
			if (!IsVisibleToSurvivors(i))
			{
				RemoveEdict(i);
				Debug_Print("Removed Zombie %i for being way behind and invisible", i);
				if (GetConVarBool(cvarRespawnRemovedCI))
				{
					CheatCommand(0, "z_spawn", "infected auto");
				}
			}
			else
			{
				Debug_Print("Found Zombie %i way behind but visible", i);
			}			
		}
	}
}

bool:IsVisibleToSurvivors(entity) // loops alive Survivors and checks entity for being visible
{
	for (new i = 1; i <= survivorCount; i++) // this loop finds us the lowest Survivor Flow
	{
		if (!IsValidEntity(survivorIndex[i]))
		{
			StateHelpers_RebuildSurvIndex(); // play it safe, if the Survivor Index is off, dont erase the zombie in any case
			return true;
		}
		
		if (IsVisibleTo(survivorIndex[i], entity))return true;
	}
	return false;
}

bool:IsVisibleTo(client, entity) // check an entity for being visible to a client
{
	decl Float:vAngles[3], Float:vOrigin[3], Float:vEnt[3], Float:vLookAt[3];
	
	GetClientEyePosition(client,vOrigin); // get both player and zombie position
	GetEntityAbsOrigin(entity, vEnt);
	
	MakeVectorFromPoints(vOrigin, vEnt, vLookAt); // compute vector from player to zombie
	
	GetVectorAngles(vLookAt, vAngles); // get angles from vector for trace
	
	// execute Trace
	new Handle:trace = TR_TraceRayFilterEx(vOrigin, vAngles, MASK_SHOT, RayType_Infinite, DespawnInfected_TraceFilter);
	
	if(TR_DidHit(trace))
	{
		decl Float:vStart[3];
		TR_GetEndPosition(vStart, trace); // retrieve our trace endpoint
		
		if ( (GetVectorDistance(vOrigin, vStart, false) + TRACE_TOLERANCE) >= GetVectorDistance(vOrigin, vEnt))
		{
			return true; // if trace ray lenght plus tolerance equal or bigger absolute distance, you hit the targeted zombie
		}
	}
	else
	{
		Debug_Print("[SRSMOD] Zombie Despawner Bug: Player-Zombie Trace did not hit anything, WTF");
		CloseHandle(trace);
		return true;
	}
	CloseHandle(trace);
	return false;
}

public bool:DespawnInfected_TraceFilter(entity, contentsMask)
{
	if (!entity || entity < 32 || !IsValidEntity(entity)) // dont let WORLD, players, or invalid entities be hit
	{
		return false;
	}
	
	decl String:class[128];
	GetEdictClassname(entity, class, sizeof(class)); // also not zombies or witches, as unlikely that may be
	if (!StrEqual(class, "infected", false) || !StrEqual(class, "witch", false))
	{
		return false;
	}
	
	return true;
}

#undef TRACE_TOLERANCE
#undef MODULE_NAME
#undef GAMECONFIG_FILE